package crawler

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"math/rand"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	"MyBlogv2/common/milvus"
	minio "MyBlogv2/common/minio"
	"MyBlogv2/common/models"

	"github.com/PuerkitoBio/goquery"
)

// CSDNArticle 表示CSDN博客文章的结构
type CSDNArticle struct {
	Title       string    `json:"title"`        // 文章标题
	Author      string    `json:"author"`       // 作者
	Avatar      string    `json:"avatar"`       // 作者头像URL
	PublishTime string    `json:"publish_time"` // 发布时间
	Content     string    `json:"content"`      // 文章内容
	Tags        []string  `json:"tags"`         // 标签
	URL         string    `json:"url"`          // 文章URL
	CrawledAt   time.Time `json:"crawled_at"`   // 爬取时间
}

// CSDNCrawler CSDN爬虫结构
type CSDNCrawler struct {
	client            *http.Client
	headers           map[string]string
	maxRetries        int
	retryDelay        time.Duration
	enableImageUpload bool // 是否启用图片上传到MinIO
}

// NewCSDNCrawler 创建新的CSDN爬虫实例
func NewCSDNCrawler() *CSDNCrawler {
	return &CSDNCrawler{
		client: &http.Client{
			Timeout: 30 * time.Second,
		},
		headers: map[string]string{
			"User-Agent":      "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
			"Accept":          "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
			"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
		},
		maxRetries:        3,
		retryDelay:        2 * time.Second,
		enableImageUpload: true, // 默认启用图片上传
	}
}

// NewCSDNCrawlerWithOptions 创建带选项的CSDN爬虫实例
func NewCSDNCrawlerWithOptions(enableImageUpload bool) *CSDNCrawler {
	crawler := NewCSDNCrawler()
	crawler.enableImageUpload = enableImageUpload
	return crawler
}

// CrawlArticle 爬取指定URL的CSDN文章
func (c *CSDNCrawler) CrawlArticle(url string) (*CSDNArticle, error) {
	// 验证URL格式
	if !c.isValidCSDNURL(url) {
		return nil, fmt.Errorf("无效的CSDN URL: %s", url)
	}

	var lastErr error
	for attempt := 0; attempt <= c.maxRetries; attempt++ {
		if attempt > 0 {
			log.Printf("重试第 %d 次爬取: %s", attempt, url)
			time.Sleep(c.retryDelay)
		}

		article, err := c.crawlArticleOnce(url)
		if err == nil {
			return article, nil
		}

		lastErr = err
		log.Printf("第 %d 次尝试失败: %v", attempt+1, err)
	}

	return nil, fmt.Errorf("爬取失败，已重试 %d 次: %v", c.maxRetries, lastErr)
}

// crawlArticleOnce 单次爬取文章
func (c *CSDNCrawler) crawlArticleOnce(url string) (*CSDNArticle, error) {
	// 创建HTTP请求
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, fmt.Errorf("创建请求失败: %v", err)
	}

	// 设置请求头
	for key, value := range c.headers {
		req.Header.Set(key, value)
	}

	// 发送请求
	resp, err := c.client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("请求失败: %v", err)
	}
	defer resp.Body.Close()

	// 检查响应状态
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("请求失败，状态码: %d", resp.StatusCode)
	}

	// 读取响应内容
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("读取响应失败: %v", err)
	}

	// // 保存HTML内容到本地文件（用于调试和分析）
	// if err := c.saveHTMLToFile(body, url); err != nil {
	// 	log.Printf("保存HTML文件失败: %v", err)
	// }

	// 解析HTML
	doc, err := goquery.NewDocumentFromReader(strings.NewReader(string(body)))
	if err != nil {
		return nil, fmt.Errorf("解析HTML失败: %v", err)
	}

	// 提取文章信息
	article := &CSDNArticle{
		URL:       url,
		CrawledAt: time.Now(),
	}

	// 提取标题
	article.Title = c.extractTitle(doc)

	// 提取作者
	article.Author = c.extractAuthor(doc)

	// 提取作者头像
	article.Avatar = c.extractAvatar(doc)

	// 提取发布时间
	article.PublishTime = c.extractPublishTime(doc)

	// 提取文章内容
	article.Content = c.extractContent(doc)

	// 提取标签
	article.Tags = c.extractTags(doc)

	return article, nil
}

// isValidCSDNURL 验证是否为有效的CSDN URL
func (c *CSDNCrawler) isValidCSDNURL(url string) bool {
	pattern := `^https://blog\.csdn\.net/[^/]+/article/details/\d+`
	matched, _ := regexp.MatchString(pattern, url)
	return matched
}

// extractTitle 提取文章标题
func (c *CSDNCrawler) extractTitle(doc *goquery.Document) string {
	// 尝试多种选择器
	selectors := []string{
		"h1.title-article",
		"h1#articleContentId",
		"h1",
		"title",
	}

	for _, selector := range selectors {
		title := doc.Find(selector).First().Text()
		if strings.TrimSpace(title) != "" {
			return strings.TrimSpace(title)
		}
	}

	return "未找到标题"
}

// extractAuthor 提取作者信息
func (c *CSDNCrawler) extractAuthor(doc *goquery.Document) string {
	// 尝试多种选择器
	selectors := []string{
		".profile-name",    // CSDN个人主页昵称
		".follow-nickName",
		".user-name",
		".author",
		"span.name",        // 文章页面作者名称
		"[data-nickname]",
	}

	for _, selector := range selectors {
		author := doc.Find(selector).First().Text()
		if strings.TrimSpace(author) != "" {
			return strings.TrimSpace(author)
		}
	}

	// 从URL中提取用户名
	url := doc.Find("meta[property='og:url']").AttrOr("content", "")
	if url != "" {
		parts := strings.Split(url, "/")
		if len(parts) >= 4 {
			return parts[3] // 通常是用户名
		}
	}

	return "未知作者"
}

// extractAvatar 提取作者头像URL
func (c *CSDNCrawler) extractAvatar(doc *goquery.Document) string {
	// 尝试多种选择器
	selectors := []string{
		".profile-box img.profile-img",  // 工具栏中的头像
		".avatar_pic",                   // 侧边栏中的头像
		".avatar-box img",               // 头像框中的图片
		"img[alt='avatar']",             // 带alt属性的头像
	}

	for _, selector := range selectors {
		avatarURL, exists := doc.Find(selector).First().Attr("src")
		if exists && strings.TrimSpace(avatarURL) != "" {
			// 处理相对URL
			if strings.HasPrefix(avatarURL, "//") {
				avatarURL = "https:" + avatarURL
			} else if strings.HasPrefix(avatarURL, "/") {
				avatarURL = "https://blog.csdn.net" + avatarURL
			}
			return strings.TrimSpace(avatarURL)
		}
	}

	// 尝试从JavaScript变量中提取头像URL
	var avatarFromScript string
	doc.Find("script").Each(func(i int, s *goquery.Selection) {
		if avatarFromScript != "" {
			return // 已找到，跳过后续脚本
		}
		scriptContent := s.Text()
		// 查找 var avatar = "..." 模式
		re := regexp.MustCompile(`var\s+avatar\s*=\s*["']([^"']+)["']`)
		matches := re.FindStringSubmatch(scriptContent)
		if len(matches) > 1 {
			avatarURL := strings.TrimSpace(matches[1])
			if avatarURL != "" {
				avatarFromScript = avatarURL
			}
		}
	})

	if avatarFromScript != "" {
		return avatarFromScript
	}

	return ""
}

// extractPublishTime 提取发布时间
func (c *CSDNCrawler) extractPublishTime(doc *goquery.Document) string {
	// 尝试多种选择器
	selectors := []string{
		".time",
		".publish-time",
		"[data-time]",
		"time",
	}

	for _, selector := range selectors {
		timeStr := doc.Find(selector).First().Text()
		if strings.TrimSpace(timeStr) != "" {
			return strings.TrimSpace(timeStr)
		}
	}

	return "未知时间"
}

// extractContent 提取文章内容（保留Markdown格式）
func (c *CSDNCrawler) extractContent(doc *goquery.Document) string {
	// 尝试多种选择器
	selectors := []string{
		"#article_content",
		".article_content",
		".markdown_views",
		"#content_views",
		".blog-content-box",
	}

	for _, selector := range selectors {
		content := doc.Find(selector).First()
		if content.Length() > 0 {
			// 移除脚本和样式标签
			content.Find("script, style").Remove()

			// 转换为Markdown格式
			markdown := c.htmlToMarkdown(content)
			if strings.TrimSpace(markdown) != "" {
				return strings.TrimSpace(markdown)
			}
		}
	}

	return "未找到内容"
}

// htmlToMarkdown 将HTML内容转换为Markdown格式
func (c *CSDNCrawler) htmlToMarkdown(selection *goquery.Selection) string {
	var result strings.Builder

	// 遍历所有子节点
	selection.Contents().Each(func(i int, s *goquery.Selection) {
		if s.Is("text") {
			// 处理文本节点
			text := strings.TrimSpace(s.Text())
			if text != "" {
				result.WriteString(text)
			}
		} else {
			// 处理HTML标签
			tagName := goquery.NodeName(s)
			switch tagName {
			case "h1":
				result.WriteString("\n# " + strings.TrimSpace(s.Text()) + "\n\n")
			case "h2":
				result.WriteString("\n## " + strings.TrimSpace(s.Text()) + "\n\n")
			case "h3":
				result.WriteString("\n### " + strings.TrimSpace(s.Text()) + "\n\n")
			case "h4":
				result.WriteString("\n#### " + strings.TrimSpace(s.Text()) + "\n\n")
			case "h5":
				result.WriteString("\n##### " + strings.TrimSpace(s.Text()) + "\n\n")
			case "h6":
				result.WriteString("\n###### " + strings.TrimSpace(s.Text()) + "\n\n")
			case "p":
				// 处理段落，先处理图片，再处理其他内容
				// 处理段落中的图片
				s.Find("img").Each(func(i int, img *goquery.Selection) {
					src, _ := img.Attr("src")
					alt, _ := img.Attr("alt")

					// 如果 src 为空或为占位符，尝试获取懒加载属性
					if src == "" || strings.Contains(src, "data:image") || strings.Contains(src, "placeholder") {
						// 尝试常见的懒加载属性
						lazyAttrs := []string{"data-src", "data-original", "data-lazy", "data-url"}
						for _, attr := range lazyAttrs {
							if lazySrc, exists := img.Attr(attr); exists && lazySrc != "" {
								src = lazySrc
								break
							}
						}
					}

					// 处理相对URL，转换为绝对URL
					if src != "" && strings.HasPrefix(src, "//") {
						src = "https:" + src
					} else if src != "" && strings.HasPrefix(src, "/") {
						src = "https://blog.csdn.net" + src
					}

					if src != "" {
						// 处理图片URL，下载并上传到MinIO
						newSrc, err := c.processImageURL(src)
						if err != nil {
							log.Printf("处理图片URL失败: %v", err)
							newSrc = src // 使用原URL
						}

						if alt != "" {
							result.WriteString("![" + alt + "](" + newSrc + ")\n\n")
						} else {
							result.WriteString("![](" + newSrc + ")\n\n")
						}
					}
				})

				// 移除图片标签后处理其他内容
				s.Clone().Find("img").Remove()
				text := strings.TrimSpace(s.Text())
				if text != "" {
					result.WriteString(text + "\n\n")
				}
			case "br":
				result.WriteString("\n")
			case "strong", "b":
				result.WriteString("**" + strings.TrimSpace(s.Text()) + "**")
			case "em", "i":
				result.WriteString("*" + strings.TrimSpace(s.Text()) + "*")
			case "code":
				result.WriteString("`" + strings.TrimSpace(s.Text()) + "`")
			case "pre":
				// 处理代码块
				code := strings.TrimSpace(s.Text())
				if code != "" {
					result.WriteString("\n```\n" + code + "\n```\n\n")
				}
			case "blockquote":
				text := strings.TrimSpace(s.Text())
				if text != "" {
					lines := strings.Split(text, "\n")
					for _, line := range lines {
						if strings.TrimSpace(line) != "" {
							result.WriteString("> " + strings.TrimSpace(line) + "\n")
						}
					}
					result.WriteString("\n")
				}
			case "ul":
				s.Find("li").Each(func(i int, li *goquery.Selection) {
					text := strings.TrimSpace(li.Text())
					if text != "" {
						result.WriteString("- " + text + "\n")
					}
				})
				result.WriteString("\n")
			case "ol":
				s.Find("li").Each(func(i int, li *goquery.Selection) {
					text := strings.TrimSpace(li.Text())
					if text != "" {
						result.WriteString(fmt.Sprintf("%d. %s\n", i+1, text))
					}
				})
				result.WriteString("\n")
			case "li":
				// 单独处理li标签（当它不在ul或ol中时）
				text := strings.TrimSpace(s.Text())
				if text != "" {
					result.WriteString("- " + text + "\n")
				}
			case "a":
				href, exists := s.Attr("href")
				text := strings.TrimSpace(s.Text())
				if exists && text != "" {
					result.WriteString("[" + text + "](" + href + ")")
				} else if text != "" {
					result.WriteString(text)
				}
			case "img":
				// 处理图片标签
				src, _ := s.Attr("src")
				alt, _ := s.Attr("alt")

				// 如果 src 为空或为占位符，尝试获取懒加载属性
				if src == "" || strings.Contains(src, "data:image") || strings.Contains(src, "placeholder") {
					// 尝试常见的懒加载属性
					lazyAttrs := []string{"data-src", "data-original", "data-lazy", "data-url"}
					for _, attr := range lazyAttrs {
						if lazySrc, exists := s.Attr(attr); exists && lazySrc != "" {
							src = lazySrc
							break
						}
					}
				}

				// 处理相对URL，转换为绝对URL
				if src != "" && strings.HasPrefix(src, "//") {
					src = "https:" + src
				} else if src != "" && strings.HasPrefix(src, "/") {
					src = "https://blog.csdn.net" + src
				}

				if src != "" {
					// 处理图片URL，下载并上传到MinIO
					newSrc, err := c.processImageURL(src)
					if err != nil {
						log.Printf("处理图片URL失败: %v", err)
						newSrc = src // 使用原URL
					}

					if alt != "" {
						result.WriteString("![" + alt + "](" + newSrc + ")\n\n")
					} else {
						result.WriteString("![](" + newSrc + ")\n\n")
					}
				}
			case "hr":
				result.WriteString("\n---\n\n")
			case "table":
				// 简单处理表格
				result.WriteString("\n")
				s.Find("tr").Each(func(i int, tr *goquery.Selection) {
					var cells []string
					tr.Find("td, th").Each(func(j int, cell *goquery.Selection) {
						cells = append(cells, strings.TrimSpace(cell.Text()))
					})
					if len(cells) > 0 {
						result.WriteString("| " + strings.Join(cells, " | ") + " |\n")
						// 添加表头分隔线
						if i == 0 {
							separator := make([]string, len(cells))
							for k := range separator {
								separator[k] = "---"
							}
							result.WriteString("| " + strings.Join(separator, " | ") + " |\n")
						}
					}
				})
				result.WriteString("\n")
			default:
				// 对于其他标签，递归处理其内容
				content := c.htmlToMarkdown(s)
				result.WriteString(content)
			}
		}
	})

	return result.String()
}

// extractTags 提取文章标签
func (c *CSDNCrawler) extractTags(doc *goquery.Document) []string {
	var tags []string

	// 尝试多种选择器
	selectors := []string{
		".tag-link",
		".tags-box a",
		".tag-list a",
		"[data-tag]",
	}

	for _, selector := range selectors {
		doc.Find(selector).Each(func(i int, s *goquery.Selection) {
			tag := strings.TrimSpace(s.Text())
			if tag != "" {
				tags = append(tags, tag)
			}
		})
		if len(tags) > 0 {
			break
		}
	}

	return tags
}

// CrawlArticleToJSON 爬取文章并返回JSON格式
func (c *CSDNCrawler) CrawlArticleToJSON(url string) (string, error) {
	article, err := c.CrawlArticle(url)
	if err != nil {
		return "", err
	}

	jsonData, err := json.MarshalIndent(article, "", "  ")
	if err != nil {
		return "", fmt.Errorf("JSON序列化失败: %v", err)
	}

	return string(jsonData), nil
}

// BatchCrawlArticles 批量爬取文章
func (c *CSDNCrawler) BatchCrawlArticles(urls []string) ([]*CSDNArticle, error) {
	var articles []*CSDNArticle
	var errors []string

	for i, url := range urls {
		log.Printf("正在爬取第 %d/%d 篇文章: %s", i+1, len(urls), url)

		article, err := c.CrawlArticle(url)
		if err != nil {
			log.Printf("爬取失败: %s, 错误: %v", url, err)
			errors = append(errors, fmt.Sprintf("%s: %v", url, err))
			continue
		}

		articles = append(articles, article)

		// 添加延迟避免请求过于频繁
		time.Sleep(1 * time.Second)
	}

	if len(errors) > 0 {
		log.Printf("部分文章爬取失败: %v", errors)
	}

	return articles, nil
}

// SaveArticleToFile 将文章保存到文件
func (c *CSDNCrawler) SaveArticleToFile(article *CSDNArticle, filename string) error {
	jsonData, err := json.MarshalIndent(article, "", "  ")
	if err != nil {
		return fmt.Errorf("JSON序列化失败: %v", err)
	}

	err = os.WriteFile(filename, jsonData, 0644)
	if err != nil {
		return fmt.Errorf("写入文件失败: %v", err)
	}

	return nil
}

// SaveArticlesToFiles 批量保存文章到文件
func (c *CSDNCrawler) SaveArticlesToFiles(articles []*CSDNArticle, outputDir string) error {
	// 创建输出目录
	if err := os.MkdirAll(outputDir, 0755); err != nil {
		return fmt.Errorf("创建目录失败: %v", err)
	}

	for i, article := range articles {
		// 生成文件名（使用文章标题，清理特殊字符）
		filename := c.sanitizeFilename(article.Title)
		if filename == "" {
			filename = fmt.Sprintf("article_%d", i+1)
		}
		filename = filepath.Join(outputDir, filename+".json")

		// 保存文章
		if err := c.SaveArticleToFile(article, filename); err != nil {
			log.Printf("保存文章失败 %s: %v", filename, err)
			continue
		}

		log.Printf("文章已保存到: %s", filename)
	}

	return nil
}

// SanitizeFilename 清理文件名中的特殊字符（公开方法）
func (c *CSDNCrawler) SanitizeFilename(filename string) string {
	return c.sanitizeFilename(filename)
}

// sanitizeFilename 清理文件名中的特殊字符
func (c *CSDNCrawler) sanitizeFilename(filename string) string {
	// 移除或替换特殊字符
	filename = strings.ReplaceAll(filename, "/", "_")
	filename = strings.ReplaceAll(filename, "\\", "_")
	filename = strings.ReplaceAll(filename, ":", "_")
	filename = strings.ReplaceAll(filename, "*", "_")
	filename = strings.ReplaceAll(filename, "?", "_")
	filename = strings.ReplaceAll(filename, "\"", "_")
	filename = strings.ReplaceAll(filename, "<", "_")
	filename = strings.ReplaceAll(filename, ">", "_")
	filename = strings.ReplaceAll(filename, "|", "_")

	// 限制文件名长度
	if len(filename) > 100 {
		filename = filename[:100]
	}

	return strings.TrimSpace(filename)
}

// CrawlAndSave 爬取文章并保存到文件
func (c *CSDNCrawler) CrawlAndSave(url, outputDir string) error {
	article, err := c.CrawlArticle(url)
	if err != nil {
		return err
	}

	// 创建输出目录
	if err := os.MkdirAll(outputDir, 0755); err != nil {
		return fmt.Errorf("创建目录失败: %v", err)
	}

	// 生成文件名
	filename := c.sanitizeFilename(article.Title)
	if filename == "" {
		filename = "article"
	}
	filename = filepath.Join(outputDir, filename+".json")

	// 保存文章
	return c.SaveArticleToFile(article, filename)
}

func (c *CSDNCrawler) CrawlAndSaveToDatabaseWithUrlAndCategoryName(url string, categoryName string) error {
	article, err := c.CrawlArticle(url)
	if err != nil {
		return err
	}

	// 检查或创建用户，获取用户ID
	userId, err := c.GetOrCreateUserByNickname(article.Author, article.Avatar)
	if err != nil {
		return fmt.Errorf("获取或创建用户失败: %v", err)
	}
	log.Printf("使用用户ID: %d (昵称: %s)", userId, article.Author)

	// 创建Markdown内容
	markdownContent := fmt.Sprintf(`# %s

**作者**: %s  
**发布时间**: %s  
**标签**: %s  
**原文链接**: %s  
**爬取时间**: %s

---

%s`,
		article.Title,
		article.Author,
		article.PublishTime,
		strings.Join(article.Tags, ", "),
		article.URL,
		article.CrawledAt.Format("2006-01-02 15:04:05"),
		article.Content)

	var articleDb models.Article
	articleDb.ArticleTitle = article.Title
	articleDb.UserId = userId // 使用爬取文章作者的用户ID
	articleDb.ArticleContent = markdownContent
	articleDb.ArticleCover = GetArticleCoverRandom()
	articleDb.CategoryId = c.GetCategoryIdByCategoryName(categoryName)
	articleDb.ArticleType = 2
	articleDb.IsTop = 0
	articleDb.Status = 1
	articleDb.IsDeleted = 0
	models.Db.Model(new(models.Article)).Create(&articleDb)
	log.Printf("文章已保存到数据库: %d (作者ID: %d)", articleDb.Id, userId)

	tagsIds, err := c.ProcessTagsToDatabase(article.Tags)
	if err != nil {
		return err
	}
	c.AddTagsToArticleTag(articleDb.Id, tagsIds)
	log.Printf("标签已添加到文章: %v", tagsIds)
	milvus.MilvusDB.UpsertArticleToMilvus(articleDb)
	return nil
}

func (c *CSDNCrawler) GetCategoryIdByCategoryName(categoryName string) int {
	var category models.Category
	var count int64
	models.Db.Model(new(models.Category)).Where("category_name = ?", categoryName).Count(&count)
	// 如果categoryName不存在，则创建
	if count == 0 {
		models.Db.Model(new(models.Category)).Create(&models.Category{CategoryName: categoryName})
	}
	// 如果categoryName存在，则获取
	models.Db.Model(new(models.Category)).Where("category_name = ?", categoryName).First(&category)
	return category.Id
}

func GetArticleCoverRandom() string {
	covers := []string{"http://47.100.111.28:9000/myblogv2/wallhaven-ly8ykq.jpg",
		"http://47.100.111.28:9000/myblogv2/wallhaven-ml3prm.jpg",
		"http://47.100.111.28:9000/myblogv2/wallhaven-qr2wd5.png"}
	return covers[rand.Intn(len(covers))]
}

// SaveMarkdownToFile 保存Markdown内容到文件
func (c *CSDNCrawler) SaveMarkdownToFile(content, filename string) error {
	err := os.WriteFile(filename, []byte(content), 0644)
	if err != nil {
		return fmt.Errorf("写入Markdown文件失败: %v", err)
	}
	return nil
}

// CrawlAndSaveAsMarkdown 爬取文章并保存为Markdown文件
func (c *CSDNCrawler) CrawlAndSaveAsMarkdown(url, outputDir string) error {
	article, err := c.CrawlArticle(url)
	if err != nil {
		return err
	}

	// 创建输出目录
	if err := os.MkdirAll(outputDir, 0755); err != nil {
		return fmt.Errorf("创建目录失败: %v", err)
	}

	// 生成Markdown文件名
	filename := c.sanitizeFilename(article.Title)
	if filename == "" {
		filename = "article"
	}
	filename = filepath.Join(outputDir, filename+".md")

	// 创建Markdown文件内容
	markdownContent := fmt.Sprintf(`# %s

**作者**: %s  
**发布时间**: %s  
**标签**: %s  
**原文链接**: %s  
**爬取时间**: %s

---

%s`,
		article.Title,
		article.Author,
		article.PublishTime,
		strings.Join(article.Tags, ", "),
		article.URL,
		article.CrawledAt.Format("2006-01-02 15:04:05"),
		article.Content)

	// 保存Markdown文件
	return c.SaveMarkdownToFile(markdownContent, filename)
}

// saveHTMLToFile 保存HTML内容到本地文件（用于调试和分析）
func (c *CSDNCrawler) saveHTMLToFile(body []byte, url string) error {
	// 创建HTML调试目录
	debugDir := "./debug_html"
	if err := os.MkdirAll(debugDir, 0755); err != nil {
		return fmt.Errorf("创建调试目录失败: %v", err)
	}

	// 从URL中提取文章ID作为文件名
	parts := strings.Split(url, "/")
	var articleID string
	if len(parts) > 0 {
		articleID = parts[len(parts)-1]
	} else {
		articleID = "unknown"
	}

	// 生成文件名，包含时间戳
	timestamp := time.Now().Format("20060102_150405")
	filename := fmt.Sprintf("%s_%s.html", articleID, timestamp)
	filepath := filepath.Join(debugDir, filename)

	// 创建HTML文件内容，添加一些调试信息
	htmlContent := fmt.Sprintf(`<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>CSDN文章HTML调试 - %s</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        .debug-info { background: #f0f0f0; padding: 10px; margin-bottom: 20px; border-radius: 5px; }
        .debug-info h3 { margin-top: 0; color: #333; }
        .debug-info p { margin: 5px 0; }
    </style>
</head>
<body>
    <div class="debug-info">
        <h3>调试信息</h3>
        <p><strong>原始URL:</strong> %s</p>
        <p><strong>爬取时间:</strong> %s</p>
        <p><strong>文件大小:</strong> %d 字节</p>
    </div>
    <hr>
    %s
</body>
</html>`,
		articleID,
		url,
		time.Now().Format("2006-01-02 15:04:05"),
		len(body),
		string(body))

	// 保存文件
	err := os.WriteFile(filepath, []byte(htmlContent), 0644)
	if err != nil {
		return fmt.Errorf("写入HTML文件失败: %v", err)
	}

	log.Printf("HTML调试文件已保存到: %s", filepath)
	return nil
}

// downloadImage 下载图片到临时文件
func (c *CSDNCrawler) downloadImage(imageURL string) (*os.File, error) {
	// 创建HTTP请求
	req, err := http.NewRequest("GET", imageURL, nil)
	if err != nil {
		return nil, fmt.Errorf("创建图片请求失败: %v", err)
	}

	// 设置请求头
	for key, value := range c.headers {
		req.Header.Set(key, value)
	}

	// 发送请求
	resp, err := c.client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("下载图片失败: %v", err)
	}
	defer resp.Body.Close()

	// 检查响应状态
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("下载图片失败，状态码: %d", resp.StatusCode)
	}

	// 检查Content-Type是否为图片
	contentType := resp.Header.Get("Content-Type")
	if !strings.HasPrefix(contentType, "image/") {
		return nil, fmt.Errorf("不是有效的图片类型: %s", contentType)
	}

	// 创建临时文件
	tempFile, err := os.CreateTemp("", "crawler-image-*.tmp")
	if err != nil {
		return nil, fmt.Errorf("创建临时文件失败: %v", err)
	}

	// 复制图片内容到临时文件
	_, err = io.Copy(tempFile, resp.Body)
	if err != nil {
		tempFile.Close()
		os.Remove(tempFile.Name())
		return nil, fmt.Errorf("保存图片失败: %v", err)
	}

	// 重置文件指针到开头
	_, err = tempFile.Seek(0, 0)
	if err != nil {
		tempFile.Close()
		os.Remove(tempFile.Name())
		return nil, fmt.Errorf("重置文件指针失败: %v", err)
	}

	return tempFile, nil
}

// uploadImageToMinIO 上传图片到MinIO并返回新的URL
func (c *CSDNCrawler) uploadImageToMinIO(imageFile *os.File, originalURL string) (string, error) {
	// 从原始URL中提取文件名
	parsedURL, err := url.Parse(originalURL)
	if err != nil {
		return "", fmt.Errorf("解析图片URL失败: %v", err)
	}

	// 获取文件扩展名，并清理特殊字符（如 !1, !2 等）
	ext := filepath.Ext(parsedURL.Path)
	// 处理类似 .jpg!1 这样的扩展名
	if strings.Contains(ext, "!") {
		ext = strings.Split(ext, "!")[0]
	}
	// 只保留有效的图片扩展名
	validExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp"}
	isValid := false
	for _, validExt := range validExts {
		if strings.ToLower(ext) == validExt {
			isValid = true
			break
		}
	}
	if !isValid {
		ext = ".jpg" // 默认扩展名
	}

	// 生成新的文件名
	fileName := minio.GenerateFileName() + ext

	// 上传到MinIO
	newURL, err := minio.UploadImage(fileName, imageFile)
	if err != nil {
		return "", fmt.Errorf("上传图片到MinIO失败: %v", err)
	}

	log.Printf("图片上传成功: %s -> %s", originalURL, newURL)
	return newURL, nil
}

// processImageURL 处理单个图片URL，下载并上传到MinIO
func (c *CSDNCrawler) processImageURL(imageURL string) (string, error) {
	if !c.enableImageUpload {
		return imageURL, nil // 如果未启用图片上传，直接返回原URL
	}

	// 处理相对URL
	if strings.HasPrefix(imageURL, "//") {
		imageURL = "https:" + imageURL
	} else if strings.HasPrefix(imageURL, "/") {
		imageURL = "https://blog.csdn.net" + imageURL
	}

	log.Printf("开始处理图片: %s", imageURL)

	// 下载图片
	imageFile, err := c.downloadImage(imageURL)
	if err != nil {
		log.Printf("下载图片失败 %s: %v", imageURL, err)
		return imageURL, nil // 下载失败时返回原URL
	}
	defer func() {
		imageFile.Close()
		os.Remove(imageFile.Name()) // 清理临时文件
	}()

	// 上传到MinIO
	newURL, err := c.uploadImageToMinIO(imageFile, imageURL)
	if err != nil {
		log.Printf("上传图片失败 %s: %v", imageURL, err)
		return imageURL, nil // 上传失败时返回原URL
	}

	return newURL, nil
}

// ProcessTagsToDatabase 处理tags数组，检查数据库中是否存在每个tag，如果不存在则自动插入
func (c *CSDNCrawler) ProcessTagsToDatabase(tags []string) ([]int, error) {
	if len(tags) == 0 {
		return []int{}, nil
	}

	for i, tag := range tags {
		// 如果标签开头有#号，则删除
		if strings.HasPrefix(tag, "#") {
			tags[i] = strings.TrimPrefix(tag, "#")
		}
	}

	var tagIds []int
	var existingTags []models.Tag
	var newTags []models.Tag

	// 查询数据库中已存在的tags
	err := models.Db.Where("tag_name IN ?", tags).Find(&existingTags).Error
	if err != nil {
		return nil, fmt.Errorf("查询已存在tags失败: %v", err)
	}

	// 创建已存在tags的映射
	existingTagMap := make(map[string]int)
	currentTagMap := make(map[string]int)
	for _, tag := range existingTags {
		existingTagMap[tag.TagName] = tag.Id
		tagIds = append(tagIds, tag.Id)
	}

	// 找出需要新增的tags
	for _, tagName := range tags {
		if _, exists := existingTagMap[tagName]; !exists {
			if _, exists2 := currentTagMap[tagName]; !exists2 {
				newTag := models.Tag{
					TagName: tagName,
				}
				newTags = append(newTags, newTag)
				currentTagMap[tagName] = newTag.Id
			}
		}
	}

	// 批量插入新的tags
	if len(newTags) > 0 {
		err = models.Db.Create(&newTags).Error
		if err != nil {
			return nil, fmt.Errorf("批量插入新tags失败: %v", err)
		}

		// 将新插入的tags的ID添加到结果中
		for _, tag := range newTags {
			tagIds = append(tagIds, tag.Id)
		}

		log.Printf("成功插入 %d 个新tags: %v", len(newTags), getTagNames(newTags))
	}

	log.Printf("处理tags完成，总共 %d 个tags，其中 %d 个已存在，%d 个新插入",
		len(tags), len(existingTags), len(newTags))

	return tagIds, nil
}

// getTagNames 从Tag数组中提取tag名称
func getTagNames(tags []models.Tag) []string {
	var names []string
	for _, tag := range tags {
		names = append(names, tag.TagName)
	}
	return names
}

func (c *CSDNCrawler) AddTagsToArticleTag(articleId int, tagIds []int) {
	for _, v := range tagIds {
		var articleTag models.ArticleTag
		articleTag.ArticleId = articleId
		articleTag.TagId = v
		models.Db.Model(new(models.ArticleTag)).Create(&articleTag)
	}
}

// GetOrCreateUserByNickname 根据昵称获取用户，如果不存在则创建新用户
func (c *CSDNCrawler) GetOrCreateUserByNickname(nickname string, avatarURL string) (int, error) {
	var user models.User
	
	// 先查询昵称是否存在
	err := models.Db.Where("nickname = ?", nickname).First(&user).Error
	
	if err == nil {
		// 用户已存在，返回用户ID
		log.Printf("用户已存在: %s (ID: %d)，不再爬取头像", nickname, user.Id)
		return user.Id, nil
	}
	
	// 用户不存在，需要创建新用户
	log.Printf("用户不存在，开始创建新用户: %s", nickname)
	
	// 处理头像URL，上传到MinIO
	var avatarMinIOURL string
	if avatarURL != "" {
		log.Printf("原始头像URL: %s", avatarURL)
		
		// 下载头像
		avatarFile, err := c.downloadImage(avatarURL)
		if err != nil {
			log.Printf("下载头像失败: %v，跳过头像上传", err)
		} else {
			defer func() {
				avatarFile.Close()
				os.Remove(avatarFile.Name()) // 清理临时文件
			}()
			
			// 上传到MinIO
			uploadedURL, err := c.uploadImageToMinIO(avatarFile, avatarURL)
			if err != nil {
				log.Printf("上传头像到MinIO失败: %v", err)
			} else {
				avatarMinIOURL = uploadedURL
				log.Printf("✅ 头像已上传到MinIO: %s", avatarMinIOURL)
			}
		}
	} else {
		log.Printf("⚠️ 未找到作者头像")
	}
	
	// 生成随机6位字符串作为username
	username := generateRandomUsername()
	
	// 创建新用户
	newUser := models.User{
		Nickname:     nickname,
		Username:     username,
		Gender:       0,
		Password:     "$2a$10$VyFtQ3T943p3NY5R0IxzIONjdqABmuCSGiHe5uV8d1ujLGYuS2KZe",
		Avatar:       avatarMinIOURL,
		RegisterType: 0, // 标记为爬虫创建的用户
		IsDisAble:    0,
		IsDeleted:    0,
	}
	
	err = models.Db.Create(&newUser).Error
	if err != nil {
		return 0, fmt.Errorf("创建用户失败: %v", err)
	}
	
	log.Printf("✅ 新用户创建成功: %s (ID: %d, Username: %s)", nickname, newUser.Id, username)
	return newUser.Id, nil
}

// generateRandomUsername 生成随机6位字符串作为用户名
func generateRandomUsername() string {
	const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
	result := make([]byte, 6)
	for i := range result {
		result[i] = charset[rand.Intn(len(charset))]
	}
	return string(result)
}

// stripHTMLTags 移除字符串中的HTML标签
func stripHTMLTags(input string) string {
	// 移除HTML标签
	re := regexp.MustCompile(`<[^>]*>`)
	result := re.ReplaceAllString(input, "")
	
	// 解码HTML实体
	result = strings.ReplaceAll(result, "&nbsp;", " ")
	result = strings.ReplaceAll(result, "&lt;", "<")
	result = strings.ReplaceAll(result, "&gt;", ">")
	result = strings.ReplaceAll(result, "&amp;", "&")
	result = strings.ReplaceAll(result, "&quot;", "\"")
	result = strings.ReplaceAll(result, "&#39;", "'")
	
	return strings.TrimSpace(result)
}

// SearchResult 搜索结果结构
type SearchResult struct {
	Title       string `json:"title"`        // 文章标题
	URL         string `json:"url"`          // 文章URL
	Author      string `json:"author"`       // 作者
	Description string `json:"description"`  // 文章描述
}

// CSDNSearchAPIResponse CSDN搜索API响应结构
type CSDNSearchAPIResponse struct {
	ResultVos []struct {
		Title       string `json:"title"`
		URL         string `json:"url"`
		NickName    string `json:"nickname"`
		Description string `json:"description"`
		SoType      string `json:"so_type"`
	} `json:"result_vos"`
	TotalPage float64 `json:"total_page"`
}

// SearchArticles 根据关键词搜索文章并返回文章链接列表
func (c *CSDNCrawler) SearchArticles(keyword string, maxResults int) ([]SearchResult, error) {
	if keyword == "" {
		return nil, fmt.Errorf("关键词不能为空")
	}

	if maxResults <= 0 {
		maxResults = 10 // 默认返回10条结果
	}
	
	var allResults []SearchResult
	page := 1
	pageSize := 30 // CSDN API每页返回约30条结果
	
	// 计算需要请求的页数
	maxPages := (maxResults + pageSize - 1) / pageSize
	if maxPages > 10 {
		maxPages = 10 // 限制最多请求10页（避免过多请求）
	}
	
	log.Printf("开始搜索关键词: %s, 目标结果数: %d, 预计请求页数: %d", keyword, maxResults, maxPages)
	
	// 循环请求多页直到获取足够的结果
	for page <= maxPages && len(allResults) < maxResults {
		results, err := c.searchArticlesOnePage(keyword, page)
		if err != nil {
			if page == 1 {
				// 第一页失败则返回错误
				return nil, err
			}
			// 后续页失败则停止，返回已获取的结果
			log.Printf("第%d页请求失败: %v, 停止搜索", page, err)
			break
		}
		
		if len(results) == 0 {
			// 没有更多结果了
			log.Printf("第%d页没有更多结果，停止搜索", page)
			break
		}
		
		allResults = append(allResults, results...)
		log.Printf("第%d页获取到 %d 篇文章，累计 %d 篇", page, len(results), len(allResults))
		
		// 添加延迟避免请求过于频繁
		if page < maxPages && len(allResults) < maxResults {
			time.Sleep(500 * time.Millisecond)
		}
		
		page++
	}
	
	// 截取到目标数量
	if len(allResults) > maxResults {
		allResults = allResults[:maxResults]
	}
	
	log.Printf("搜索完成，共找到 %d 篇文章", len(allResults))
	return allResults, nil
}

// searchArticlesOnePage 搜索单页结果
func (c *CSDNCrawler) searchArticlesOnePage(keyword string, page int) ([]SearchResult, error) {
	apiURL := fmt.Sprintf("https://so.csdn.net/api/v3/search?q=%s&t=blog&p=%d", url.QueryEscape(keyword), page)

	// 创建HTTP请求
	req, err := http.NewRequest("GET", apiURL, nil)
	if err != nil {
		return nil, fmt.Errorf("创建搜索请求失败: %v", err)
	}

	// 设置请求头
	req.Header.Set("User-Agent", c.headers["User-Agent"])
	req.Header.Set("Accept", "application/json, text/plain, */*")
	req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
	req.Header.Set("Referer", "https://so.csdn.net/")

	// 发送请求
	resp, err := c.client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("搜索请求失败: %v", err)
	}
	defer resp.Body.Close()

	// 检查响应状态
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("搜索请求失败，状态码: %d", resp.StatusCode)
	}

	// 读取响应内容
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("读取搜索响应失败: %v", err)
	}

	// // 保存API响应（用于调试）
	// debugDir := "./debug_search"
	// if err := os.MkdirAll(debugDir, 0755); err == nil {
	// 	timestamp := time.Now().Format("20060102_150405")
	// 	debugFile := filepath.Join(debugDir, fmt.Sprintf("search_api_%s_%s.json", keyword, timestamp))
	// 	os.WriteFile(debugFile, body, 0644)
	// 	log.Printf("搜索API响应已保存到: %s", debugFile)
	// }

	// 解析JSON响应
	var apiResp CSDNSearchAPIResponse
	if err := json.Unmarshal(body, &apiResp); err != nil {
		return nil, fmt.Errorf("解析搜索结果JSON失败: %v", err)
	}

	// 检查是否有结果
	if len(apiResp.ResultVos) == 0 {
		return nil, fmt.Errorf("未找到匹配的文章")
	}

	var results []SearchResult

	// 提取搜索结果（不限制数量，返回该页所有符合条件的结果）
	for _, item := range apiResp.ResultVos {
		// 只保留CSDN博客文章链接
		if !strings.Contains(item.URL, "blog.csdn.net") || !strings.Contains(item.URL, "/article/details/") {
			continue
		}

		// 清理HTML标签
		title := stripHTMLTags(item.Title)
		description := stripHTMLTags(item.Description)

		result := SearchResult{
			Title:       title,
			URL:         item.URL,
			Author:      item.NickName,
			Description: description,
		}

		results = append(results, result)
	}

	return results, nil
}
